/*
 * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

/*
 * @test
 * @bug 8327640 8331485 8333755 8335668
 * @summary Test suite for NumberFormat parsing with strict leniency
 * @run junit/othervm -Duser.language=en -Duser.country=US StrictParseTest
 * @run junit/othervm -Duser.language=ja -Duser.country=JP StrictParseTest
 * @run junit/othervm -Duser.language=zh -Duser.country=CN StrictParseTest
 * @run junit/othervm -Duser.language=tr -Duser.country=TR StrictParseTest
 * @run junit/othervm -Duser.language=de -Duser.country=DE StrictParseTest
 * @run junit/othervm -Duser.language=fr -Duser.country=FR StrictParseTest
 * @run junit/othervm -Duser.language=ar -Duser.country=AR StrictParseTest
 */

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

import java.text.CompactNumberFormat;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.NumberFormat;
import java.text.ParseException;
import java.text.ParsePosition;
import java.util.Locale;
import java.util.stream.Stream;

import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;

// Tests strict parsing, this is done by testing the NumberFormat factory instances
// against a number of locales with different formatting conventions. The locales
// used all use a grouping size of 3.
public class StrictParseTest {

    // Used to retrieve the locale's expected symbols
    private static final DecimalFormatSymbols dfs =
            new DecimalFormatSymbols(Locale.getDefault());
    // We re-use these formats for the respective factory tests
    private static final DecimalFormat dFmt =
            (DecimalFormat) NumberFormat.getNumberInstance(Locale.getDefault());
    private static final DecimalFormat cFmt =
            (DecimalFormat) NumberFormat.getCurrencyInstance(Locale.getDefault());
    private static final DecimalFormat pFmt =
            (DecimalFormat) NumberFormat.getPercentInstance(Locale.getDefault());
    private static final CompactNumberFormat cmpctFmt =
            (CompactNumberFormat) NumberFormat.getCompactNumberInstance(Locale.getDefault(),
                    NumberFormat.Style.SHORT);
    private static final NumberFormat[] FORMATS = new NumberFormat[]{dFmt, cFmt, pFmt, cmpctFmt};

    // Restore defaults before runs
    @BeforeEach
    void beforeEach() {
        for (NumberFormat fmt : FORMATS) {
            fmt.setStrict(true);
            fmt.setParseIntegerOnly(false);
            fmt.setGroupingUsed(true);
        }
        // Grouping Size is not defined at NumberFormat level
        // Compact needs to manually init grouping size
        cmpctFmt.setGroupingSize(3);
    }

    // ---- NumberFormat tests ----

    // Guarantee some edge case test input
    @Test // Non-localized, run once
    @EnabledIfSystemProperty(named = "user.language", matches = "en")
    public void uniqueCaseNumberFormatTest() {
        // Format with grouping size = 3, prefix = a, suffix = b
        DecimalFormat nonLocalizedDFmt = new DecimalFormat("a#,#00.00b");
        nonLocalizedDFmt.setStrict(true);
        // Text after suffix
        failParse(nonLocalizedDFmt, "a12bfoo", 3);
        failParse(nonLocalizedDFmt, "a123,456.00bc", 11);
        // Text after prefix
        failParse(nonLocalizedDFmt, "ac123", 0);
        // Missing suffix
        failParse(nonLocalizedDFmt, "a123", 4);
        // Prefix contains a decimal separator
        failParse(nonLocalizedDFmt, ".a123", 0);
        // Test non grouping size of 3
        nonLocalizedDFmt.setGroupingSize(1);
        successParse(nonLocalizedDFmt, "a1,2,3,4b");
        failParse(nonLocalizedDFmt, "a1,2,3,45,6b", 8);
        nonLocalizedDFmt.setGroupingSize(5);
        successParse(nonLocalizedDFmt, "a12345,67890b");
        successParse(nonLocalizedDFmt, "a1234,67890b");
        failParse(nonLocalizedDFmt, "a123456,7890b", 6);
    }


    // 8333755: Check that parsing with integer only against a suffix value works
    @Test // Non-localized, run once
    @EnabledIfSystemProperty(named = "user.language", matches = "en")
    public void integerOnlyParseWithSuffixTest() {
        // Some pattern with a suffix
        DecimalFormat fmt = new DecimalFormat("0.00b");
        fmt.setParseIntegerOnly(true);
        assertEquals(5d, successParse(fmt, "5.55b", 1));
        assertEquals(5d, successParse(fmt, "5b", 2));
        assertEquals(5555d, successParse(fmt, "5,555.55b", 5));
        assertEquals(5d, successParse(fmt, "5.55E55b", 1));
    }

    @Test // Non-localized, only run once
    @EnabledIfSystemProperty(named = "user.language", matches = "en")
    public void badExponentParseNumberFormatTest() {
        // Some fmt, with an "E" exponent string
        DecimalFormat fmt = (DecimalFormat) NumberFormat.getNumberInstance(Locale.US);
        fmt.setStrict(true);
        // Upon non-numeric in exponent, parse will exit early and suffix will not
        // exactly match, causing failure
        failParse(fmt, "1.23E45.1", 7);
        failParse(fmt, "1.23E45.", 7);
        failParse(fmt, "1.23E45FOO3222", 7);
    }

    // All input Strings should fail
    @ParameterizedTest
    @MethodSource("badParseStrings")
    public void numFmtFailParseTest(String toParse, int expectedErrorIndex) {
        failParse(dFmt, toParse, expectedErrorIndex);
    }

    // All input Strings should pass and return expected value.
    @ParameterizedTest
    @MethodSource("validParseStrings")
    public void numFmtSuccessParseTest(String toParse, double expectedValue) {
        assertEquals(expectedValue, successParse(dFmt, toParse));
    }

    // All input Strings should fail
    @ParameterizedTest
    @MethodSource("negativeBadParseStrings")
    public void negNumFmtFailParseTest(String toParse, int expectedErrorIndex) {
        failParse(dFmt, toParse, expectedErrorIndex);
    }

    // All input Strings should pass and return expected value.
    @ParameterizedTest
    @MethodSource("negativeValidParseStrings")
    public void negNumFmtSuccessParseTest(String toParse, double expectedValue) {
        assertEquals(expectedValue, successParse(dFmt, toParse));
    }

    // Exception should be thrown if grouping separator occurs anywhere
    // Don't pass badParseStrings as a data source, since they may fail for other reasons
    @ParameterizedTest
    @MethodSource({"validParseStrings", "noGroupingParseStrings"})
    public void numFmtStrictGroupingNotUsed(String toParse) {
        // When grouping is not used, if a grouping separator is found,
        // a failure should occur
        dFmt.setGroupingUsed(false);
        int failIndex = toParse.indexOf(
                dFmt.getDecimalFormatSymbols().getGroupingSeparator());
        if (failIndex > -1) {
            failParse(dFmt, toParse, failIndex);
        } else {
            successParse(dFmt, toParse);
        }
    }

    // 8333755: Parsing behavior should follow normal strict behavior
    // However the index returned, should be before decimal point
    // and the value parsed equal to the integer portion
    @ParameterizedTest
    @MethodSource("validIntegerOnlyParseStrings")
    public void numFmtStrictIntegerOnlyUsedTest(String toParse, Number expVal) {
        dFmt.setParseIntegerOnly(true);
        int expectedIndex = toParse.indexOf(dfs.getDecimalSeparator());
        if (expectedIndex > -1) {
            assertEquals(successParse(dFmt, toParse, expectedIndex), expVal);
        } else {
            assertEquals(successParse(dFmt, toParse), expVal);
        }
    }

    // 8335668: Parsing with integer only against String with no integer portion
    // should fail, not return 0. Expected error index should be 0
    @Test
    public void integerParseOnlyFractionOnlyTest() {
        var fmt = NumberFormat.getIntegerInstance();
        failParse(fmt, localizeText("."), 0);
        failParse(fmt, localizeText(".0"), 0);
        failParse(fmt, localizeText(".55"), 0);
    }

    // 8335668: Parsing with integer only against String with no integer portion
    // should fail, not return 0. Expected error index should be 0
    @Test // Non-localized, run once
    @EnabledIfSystemProperty(named = "user.language", matches = "en")
    public void compactIntegerParseOnlyFractionOnlyTest() {
        var fmt = NumberFormat.getCompactNumberInstance(Locale.US, NumberFormat.Style.SHORT);
        fmt.setParseIntegerOnly(true);
        failParse(fmt, ".K", 0);
        failParse(fmt, ".0K", 0);
        failParse(fmt, ".55K", 0);
    }

    // 8333755: Parsing behavior should follow normal strict behavior
    // when it comes to failures.
    @ParameterizedTest
    @MethodSource("badParseStrings")
    public void numFmtStrictIntegerOnlyUsedFailTest(String toParse, int expectedErrorIndex) {
        dFmt.setParseIntegerOnly(true);
        failParse(dFmt, toParse, expectedErrorIndex);
    }

    // ---- CurrencyFormat tests ----
    @ParameterizedTest
    @MethodSource("currencyBadParseStrings")
    public void currFmtFailParseTest(String toParse, int expectedErrorIndex) {
        failParse(cFmt, toParse, expectedErrorIndex);
    }

    @ParameterizedTest
    @MethodSource("currencyValidParseStrings")
    public void currFmtSuccessParseTest(String toParse, double expectedValue) {
        assertEquals(expectedValue, successParse(cFmt, toParse));
    }

    // ---- PercentFormat tests ----
    @ParameterizedTest
    @MethodSource("percentBadParseStrings")
    public void percentFmtFailParseTest(String toParse, int expectedErrorIndex) {
        failParse(pFmt, toParse, expectedErrorIndex);
    }

    @ParameterizedTest
    @MethodSource("percentValidParseStrings")
    public void percentFmtSuccessParseTest(String toParse, double expectedValue) {
        assertEquals(expectedValue, successParse(pFmt, toParse));
    }

    // ---- CompactNumberFormat tests ----
    // Can match to both the decimalFormat patterns and the compact patterns
    // Thus we test leniency for both. Unlike the other tests, this test
    // is only ran against the US Locale and tests against data built with the
    // thousands format (K).
    @ParameterizedTest
    @MethodSource("compactBadParseStrings")
    @EnabledIfSystemProperty(named = "user.language", matches = "en")
    public void compactFmtFailParseTest(String toParse, int expectedErrorIndex) {
        failParse(cmpctFmt, toParse, expectedErrorIndex);
    }

    @ParameterizedTest
    @MethodSource("compactValidParseStrings")
    @EnabledIfSystemProperty(named = "user.language", matches = "en")
    public void compactFmtSuccessParseTest(String toParse, double expectedValue) {
        assertEquals(expectedValue, successParse(cmpctFmt, toParse));
    }

    // Checks some odd leniency edge cases between matching of default pattern
    // and compact pattern.
    @Test // Non-localized, run once
    @EnabledIfSystemProperty(named = "user.language", matches = "en")
    public void compactFmtEdgeParseTest() {
        // Uses a compact format with unique and non-empty prefix/suffix for both
        // default and compact patterns
        CompactNumberFormat cnf = new CompactNumberFormat("a##0.0#b", DecimalFormatSymbols
                .getInstance(Locale.US), new String[]{"", "c0d"});
        cnf.setStrict(true);

        // Existing behavior of failed prefix parsing has errorIndex return
        // the beginning of prefix, even if the error occurred later in the prefix.
        // Prefix empty
        failParse(cnf, "12345d", 0);
        failParse(cnf, "1b", 0);
        // Prefix bad
        failParse(cnf, "aa1d", 0);
        failParse(cnf, "cc1d", 0);
        failParse(cnf, "aa1b", 0);
        failParse(cnf, "cc1b", 0);

        // Suffix error index is always the start of the failed suffix
        // not necessarily where the error occurred in the suffix. This is
        // consistent with the prefix error index behavior.
        // Suffix empty
        failParse(cnf, "a1", 2);
        failParse(cnf, "c1", 2);
        // Suffix bad
        failParse(cnf, "a1dd", 2);
        failParse(cnf, "c1dd", 2);
        failParse(cnf, "a1bb", 2);
        failParse(cnf, "c1bb", 2);
    }

    @ParameterizedTest
    @MethodSource({"validIntegerOnlyParseStrings", "compactValidIntegerOnlyParseStrings"})
    @EnabledIfSystemProperty(named = "user.language", matches = "en")
    public void compactFmtSuccessParseIntOnlyTest(String toParse, double expectedValue) {
        // compact does not accept exponents
        if (toParse.indexOf('E') > -1) {
            return;
        }
        cmpctFmt.setParseIntegerOnly(true);
        assertEquals(expectedValue, successParse(cmpctFmt, toParse, toParse.length()));
    }

    // Ensure that on failure, the original index of the PP remains the same
    @Test
    public void parsePositionIndexTest() {
        failParse(dFmt, localizeText("123,456,,789.00"), 8, 4);
    }

    // ---- Helper test methods ----

    // Should parse entire String successfully, and return correctly parsed value.
    private Number successParse(NumberFormat fmt, String toParse) {
        return successParse(fmt, toParse, toParse.length());
    }

    // Overloaded method that allows for an expected ParsePosition index value
    // that is not the string length.
    private Number successParse(NumberFormat fmt, String toParse, int expectedIndex) {
        // For Strings that don't have grouping separators, we test them with
        // grouping off so that they do not fail under the expectation that
        // grouping symbols should occur
        if (!toParse.contains(String.valueOf(dfs.getGroupingSeparator())) &&
                !toParse.contains(String.valueOf(dfs.getMonetaryGroupingSeparator()))) {
            fmt.setGroupingUsed(false);
        }
        Number parsedValue = assertDoesNotThrow(() -> fmt.parse(toParse));
        ParsePosition pp = new ParsePosition(0);
        assertDoesNotThrow(() -> fmt.parse(toParse, pp));
        assertEquals(-1, pp.getErrorIndex(),
                "ParsePosition ErrorIndex is not in correct location");
        assertEquals(expectedIndex, pp.getIndex(),
                "ParsePosition Index is not in correct location");
        fmt.setGroupingUsed(true);
        return parsedValue.doubleValue();
    }

    // Method which tests a parsing failure. Either a ParseException is thrown,
    // or null is returned depending on which parse method is invoked. When failing,
    // index should remain the initial index set to the ParsePosition while
    // errorIndex is the index of failure.
    private void failParse(NumberFormat fmt, String toParse, int expectedErrorIndex) {
        failParse(fmt, toParse, expectedErrorIndex, 0);
    }

    // Variant to check non 0 initial parse index
    private void failParse(NumberFormat fmt, String toParse,
                           int expectedErrorIndex, int initialParseIndex) {
        ParsePosition pp = new ParsePosition(initialParseIndex);
        assertThrows(ParseException.class, () -> fmt.parse(toParse));
        assertNull(fmt.parse(toParse, pp));
        assertEquals(expectedErrorIndex, pp.getErrorIndex());
        assertEquals(initialParseIndex, pp.getIndex());
    }

    // ---- Data Providers ----
    // These data providers use US locale grouping and decimal separators
    // for readability, however, the data is tested against multiple locales
    // and is converted appropriately at runtime.

    // Strings that should fail when parsed with strict leniency.
    // Given as Arguments<String, expectedErrorIndex>
    private static Stream<Arguments> badParseStrings() {
        return Stream.of(
                // Grouping symbol focus
                // Grouping symbol right before decimal
                Arguments.of("1,.", 2),
                Arguments.of("1,.1", 2),
                // Does not end with proper grouping size
                Arguments.of("1,1", 2),
                Arguments.of("1,11", 3),
                Arguments.of("1,1111", 5),
                Arguments.of("11,111,11", 8),
                // Does not end with proper grouping size (with decimal)
                Arguments.of("1,1.", 3),
                Arguments.of("1,11.", 4),
                Arguments.of("1,1111.", 5),
                Arguments.of("11,111,11.", 9),
                // Ends on a grouping symbol
                // Suffix matches correctly, so failure is on the ","
                Arguments.of("11,111,", 6),
                Arguments.of("11,", 2),
                Arguments.of("11,,", 3),
                // Ends with grouping symbol. Failure should occur on grouping,
                // even if non recognized char after
                Arguments.of("11,a", 2),
                // Improper grouping size (with decimal and digits after)
                Arguments.of("1,1.1", 3),
                Arguments.of("1,11.1", 4),
                Arguments.of("1,1111.1", 5),
                Arguments.of("11,111,11.1", 9),
                // Subsequent grouping symbols
                Arguments.of("1,,1", 2),
                Arguments.of("1,1,,1", 3),
                Arguments.of("1,,1,1", 2),
                // Invalid grouping sizes
                Arguments.of("1,11,111", 4),
                Arguments.of("11,11,111", 5),
                Arguments.of("111,11,11", 6),
                // First group is too large
                Arguments.of("1111,11,111", 3),
                Arguments.of("00000,11,111", 3),
                Arguments.of("111,1111111111", 7),
                Arguments.of("111,11", 5),
                Arguments.of("111,1111111111.", 7),
                Arguments.of("111,11.", 6),
                Arguments.of("111,1111111111.", 7),
                // Starts with grouping symbol
                Arguments.of(",111,,1,1", 0),
                Arguments.of(",1", 0),
                Arguments.of(",,1", 0),
                // Leading Zeros (not digits)
                Arguments.of("000,1,1", 5),
                Arguments.of("000,111,11,,1", 10),
                Arguments.of("0,000,1,,1,1", 7),
                // Bad suffix
                Arguments.of("1a", 1),
                // Bad chars in numerical portion
                Arguments.of("123a4", 3),
                Arguments.of("123.4a5", 5),
                // Variety of edge cases
                Arguments.of("123,456.77a", 10),
                Arguments.of("1,234a", 5),
                Arguments.of("1,.a", 2),
                Arguments.of("1.a", 2),
                Arguments.of("1.22a", 4),
                Arguments.of("1.1a1", 3),
                Arguments.of("1,234,a", 5),
                // Double decimal
                Arguments.of("1,234..5", 5))
                .map(args -> Arguments.of(
                        localizeText(String.valueOf(args.get()[0])), args.get()[1]));
    }

    // Strings that should parse fully. (Both in lenient and strict)
    // Given as Arguments<String, expectedParsedNumber>
    private static Stream<Arguments> validParseStrings() {
        return Stream.of(
                Arguments.of("1,234.55", 1234.55d),
                Arguments.of("1,234.5", 1234.5d),
                Arguments.of("1,234.00", 1234d),
                Arguments.of("1,234.0", 1234d),
                Arguments.of("1,234.", 1234d),
                Arguments.of("1", 1d),
                Arguments.of("10", 10d),
                Arguments.of("100", 100d),
                Arguments.of("1000", 1000d),
                Arguments.of("1,000", 1000d),
                Arguments.of("10,000", 10000d),
                Arguments.of("10000", 10000d),
                Arguments.of("100,000", 100000d),
                Arguments.of("1,000,000", 1000000d),
                Arguments.of("10,000,000", 10000000d),
                // Smaller value cases (w/ decimal)
                Arguments.of(".1", .1d),
                Arguments.of("1.1", 1.1d),
                Arguments.of("11.1", 11.1d))
                .map(args -> Arguments.of(
                        localizeText(String.valueOf(args.get()[0])), args.get()[1]));
    }

    // Separate test data set for integer only.
    // Valid parse strings, that would parse successfully for integer/non-integer parse
    private static Stream<Arguments> validIntegerOnlyParseStrings() {
        return Stream.of(
                Arguments.of("234", 234d),
                Arguments.of("234.", 234d),
                Arguments.of("234.1", 234d),
                Arguments.of("1,234.1", 1234d),
                Arguments.of("234.12345", 234d),
                Arguments.of("234.543E23", 234d),
                Arguments.of("234,000.55E22", 234000d),
                Arguments.of("234E22", 234E22))
                .map(args -> Arguments.of(localizeText(String.valueOf(args.get()[0])), args.get()[1]));
    }

    // Separate test data set for no grouping. Can not use "badParseStrings", as
    // there is test data where the failure may occur from some other issue,
    // not related to grouping
    private static Stream<Arguments> noGroupingParseStrings() {
        return Stream.of(
                Arguments.of("12,34.a"),
                Arguments.of("123,.a1"),
                Arguments.of(",1234"),
                Arguments.of("123,"))
                .map(args -> Arguments.of(localizeText(String.valueOf(args.get()[0]))));
    }

    // Negative variant of a numerical format
    private static Stream<Arguments> negativeBadParseStrings() {
        return badParseStrings().map(args -> Arguments.of(
                dFmt.getNegativePrefix() + args.get()[0] + dFmt.getNegativeSuffix(),
                (int)args.get()[1] + dFmt.getNegativePrefix().length())
        );
    }

    // Negative variant of a numerical format
    private static Stream<Arguments> negativeValidParseStrings() {
        return validParseStrings().map(args -> Arguments.of(
                dFmt.getNegativePrefix() + args.get()[0] + dFmt.getNegativeSuffix(),
                (double) args.get()[1] * -1)
        );
    }

    // Same as original with a percent prefix/suffix.
    // Additionally, increment expected error index if a prefix is added
    private static Stream<Arguments> percentBadParseStrings() {
        return badParseStrings().map(args -> Arguments.of(
                pFmt.getPositivePrefix() + args.get()[0] + pFmt.getPositiveSuffix(),
                        (int)args.get()[1] + pFmt.getPositivePrefix().length())
        );
    }

    // Expected parsed value should be / 100 as it is a percent format.
    private static Stream<Arguments> percentValidParseStrings() {
        return validParseStrings().map(args -> Arguments.of(
                pFmt.getPositivePrefix() + args.get()[0] + pFmt.getPositiveSuffix(),
                (double)args.get()[1] / 100.0)
        );
    }

    // Same as original with a currency prefix/suffix, but replace separators
    // with monetary variants. Additionally, increment expected error index
    // if a prefix is added
    private static Stream<Arguments> currencyBadParseStrings() {
        return badParseStrings().map(args -> Arguments.of(
                cFmt.getPositivePrefix() + String.valueOf(args.get()[0])
                        .replace(dfs.getGroupingSeparator(), dfs.getMonetaryGroupingSeparator())
                        .replace(dfs.getDecimalSeparator(), dfs.getMonetaryDecimalSeparator())
                        + cFmt.getPositiveSuffix(),
                (int)args.get()[1] + cFmt.getPositivePrefix().length())
        );
    }

    private static Stream<Arguments> currencyValidParseStrings() {
        return validParseStrings().map(args -> Arguments.of(
                cFmt.getPositivePrefix() + String.valueOf(args.get()[0])
                        .replace(dfs.getGroupingSeparator(), dfs.getMonetaryGroupingSeparator())
                        .replace(dfs.getDecimalSeparator(), dfs.getMonetaryDecimalSeparator())
                        + cFmt.getPositiveSuffix(),
                args.get()[1])
        );
    }

    // Compact Pattern Data Provider provides test input for both DecimalFormat patterns
    // and the compact patterns. As there is no method to retrieve compact patterns,
    // thus test only against US English locale, and use a hard coded K - 1000
    private static Stream<Arguments> compactBadParseStrings() {
        return Stream.concat(
                badParseStrings().map(args -> Arguments.of(args.get()[0], args.get()[1])),
                badParseStrings().map(args -> Arguments.of(args.get()[0] + "K", args.get()[1]))
        );
    }

    private static Stream<Arguments> compactValidParseStrings() {
        return Stream.concat(
                validParseStrings().map(args -> Arguments.of(
                        args.get()[0], args.get()[1])),
                validParseStrings().map(args -> Arguments.of(
                        args.get()[0] + "K", (double) args.get()[1] * 1000))
        );
    }

    private static Stream<Arguments> compactValidIntegerOnlyParseStrings() {
        return validIntegerOnlyParseStrings().map(args -> Arguments.of(
                args.get()[0] + "K", (double) args.get()[1] * 1000)
        );
    }

    // Replace the grouping and decimal separators with localized variants
    // Used during localization of data
    private static String localizeText(String text) {
        // As this is a single pass conversion, this is safe for multiple replacement,
        // even if a ',' could be a decimal separator for a locale.
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < text.length(); i++) {
            char c = text.charAt(i);
            if (c == ',') {
                sb.append(dfs.getGroupingSeparator());
            } else if (c == '.') {
                sb.append(dfs.getDecimalSeparator());
            } else if (c == 'E') {
                sb.append(dfs.getExponentSeparator());
            }
            else if (c == '0') {
                sb.append(dfs.getZeroDigit());
            } else {
                sb.append(c);
            }
        }
        return sb.toString();
    }
}
